/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.common.util;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

import com.facebook.common.internal.Closeables;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.robolectric.RobolectricTestRunner;

/** Tests for {@link StreamUtil} */
@RunWith(RobolectricTestRunner.class)
public class StreamUtilTest {

  /** Verify that using a ByteArrayInputStream does not allocate a new byte array. */
  @Test
  public void testByteArrayInputStream() throws Exception {
    byte[] bytes = new byte[8];
    InputStream input = new ByteArrayInputStream(bytes);
    try {
      byte[] bytesRead = StreamUtil.getBytesFromStream(input);
      assertThat(bytesRead).isEqualTo(bytes);
    } finally {
      Closeables.close(input, true);
    }
  }

  /** Verify that using an offset with ByteArrayInputStream still produces correct output. */
  @Test
  public void testByteArrayInputStreamWithOffset() throws Exception {
    byte[] bytes = new byte[] {0, 1, 2, 3, 4};
    InputStream input = new ByteArrayInputStream(bytes, 1, 4);
    try {
      byte[] bytesRead = StreamUtil.getBytesFromStream(input);
      byte[] expectedBytes = new byte[] {1, 2, 3, 4};
      assertThat(bytesRead).isEqualTo(expectedBytes);
    } finally {
      Closeables.close(input, true);
    }
  }

  /** Verify getting a byte array from a FileInputStream. */
  @Test
  public void testFileInputStream() throws Exception {
    checkFileInputStream(4);
    checkFileInputStream(64 * 1024 + 5); // Don't end on an even byte boundary
  }

  @Test
  public void testSuccessfulSkip() throws Exception {
    InputStream inputStream = mock(InputStream.class);
    when(inputStream.skip(anyLong())).thenReturn(2L);
    assertThat(StreamUtil.skip(inputStream, 10)).isEqualTo(10);
    InOrder order = inOrder(inputStream);
    order.verify(inputStream).skip(10);
    order.verify(inputStream).skip(8);
    order.verify(inputStream).skip(6);
    order.verify(inputStream).skip(4);
    order.verify(inputStream).skip(2);
    verifyNoMoreInteractions(inputStream);
  }

  @Test
  public void testUnsuccessfulSkip() throws Exception {
    InputStream inputStream = mock(InputStream.class);
    when(inputStream.skip(anyLong())).thenReturn(3L, 5L, 0L, 6L, 0L);
    when(inputStream.read()).thenReturn(3, -1);
    assertThat(StreamUtil.skip(inputStream, 20)).isEqualTo(15);
    InOrder order = inOrder(inputStream);
    order.verify(inputStream).skip(20);
    order.verify(inputStream).skip(17);
    order.verify(inputStream).skip(12);
    order.verify(inputStream).read();
    order.verify(inputStream).skip(11);
    order.verify(inputStream).skip(5);
    order.verify(inputStream).read();
    verifyNoMoreInteractions(inputStream);
  }

  private void checkFileInputStream(int size) throws IOException {
    byte[] bytesToWrite = new byte[size];
    for (int i = 0; i < size; i++) {
      bytesToWrite[i] = (byte) i; // It's okay to truncate
    }

    File tmpFile = File.createTempFile("streamUtil", "test");
    InputStream input = null;
    OutputStream output = null;
    try {
      output = new FileOutputStream(tmpFile);
      output.write(bytesToWrite);
      output.close();

      input = new FileInputStream(tmpFile);
      byte[] bytesRead = StreamUtil.getBytesFromStream(input);
      assertThat(bytesRead).isEqualTo(bytesToWrite);
    } finally {
      Closeables.close(input, true);
      Closeables.close(output, false);
      assertThat(tmpFile.delete()).isTrue();
    }
  }
}
